package org.noip.amdg;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicBoolean;

import org.noip.amdg.GameEvent.Type;
import org.noip.amdg.Problem.ProblemType;

public class GameModel {

	private final List<Bucket> buckets = new ArrayList<Bucket>();

	private final List<Bucket> notFullBuckets = new ArrayList<Bucket>();
	private final Map<Bucket, Integer> notFullBucketMap = new HashMap<Bucket, Integer>();
	private final List<Bucket> fullBuckets = new ArrayList<Bucket>();

	private static final int START_HEIGHT = 15;

	private static final int BUCKET_STEPS = 100;

	private int speed = 1;

	private AtomicBoolean pause = new AtomicBoolean(false);

	public boolean isPause() {
		return pause.get();
	}

	public static int getBucketSteps() {
		return BUCKET_STEPS;
	}

	private int score;

	public int getScore() {
		return score;
	}

	private Problem problem;

	private int height;

	private int level;

	public int getLevel() {
		return level;
	}

	private int bucket;

	private boolean running = true;

	private Thread modelThread;

	private final AtomicBoolean answerLocked = new AtomicBoolean(false);

	private final List<GameEventListener> listeners = new ArrayList<GameEventListener>();

	private int funnelPosition = 0;

	public int getFunnelPosition() {
		return funnelPosition;
	}

	public boolean isRunning() {
		return running;
	}

	public enum State {
		MENU, DROPPING_PROBLEM, MOVING_FUNNEL, DONE, LEVEL_UP
	};

	private boolean funnelMovingRight = true;

	private volatile State currentState = State.MENU;
	private AtomicBoolean changeState = new AtomicBoolean(false);

	public State getCurrentState() {
		return currentState;
	}

	private GameMode currentGameMode;

	private final SortedMap<Integer, GameMode> gameModes = new TreeMap<Integer, GameMode>();

	public SortedMap<Integer, GameMode> getGameModes() {
		return gameModes;
	}

	public GameModel() {
		int buckets = 5;
		int bucketSize = 3;

		for (int i = 0; i < buckets; i++) {
			Bucket bucket = new Bucket(bucketSize);
			this.buckets.add(bucket);
		}

		List<ProblemType> problems = new ArrayList<ProblemType>();

		problems.add(ProblemType.ADDITION);
		gameModes.put(1, new GameMode(1, "Addition",
				new ArrayList<ProblemType>(problems)));

		problems.add(ProblemType.SUBTRACTION);
		gameModes.put(2, new GameMode(2, "Addition, Subtraction",
				new ArrayList<ProblemType>(problems)));

		problems.add(ProblemType.MULTIPLICATION);
		gameModes.put(3, new GameMode(3,
				"Addition, Subtraction, Multiplication",
				new ArrayList<ProblemType>(problems)));

		problems.add(ProblemType.DIVISION);
		gameModes.put(4, new GameMode(4,
				"Addition, Subtraction, Multiplication, Division",
				new ArrayList<ProblemType>(problems)));

	}

	public void addGameEventListener(GameEventListener listener) {
		listeners.add(listener);
	}

	private void informListeners(GameEvent event) {
		for (GameEventListener listener : listeners) {
			listener.addEvent(event);
		}
	}

	public void setNewProblem() {
		ProblemType type = currentGameMode.problemTypes.get((int) (Math
				.random() * currentGameMode.problemTypes.size()));
		problem = Problem.generateProblem(type, (level - 1) / 3 + 1);
		height = START_HEIGHT;
		currentAnswer = "";
		informListeners(new GameEvent(GameEvent.Type.NEW_PROBLEM));
	}

	public boolean missProblem() {
		Bucket bucket = buckets.get(this.bucket);
		boolean done = false;
		if (bucket.isFull()) {
			done = true;
		} else if (bucket.addProblem(problem)) {
			fullBuckets.add(bucket);
			notFullBucketMap.remove(bucket);
			notFullBuckets.remove(bucket);
		}
		informListeners(new GameEvent(GameEvent.Type.INCORRECT_ANSWER));
		return done;
	}

	public void setNewBucket() {
		boolean empty = Math.random() > .8;
		if (empty) {
			bucket = notFullBucketMap.get(notFullBuckets.get((int) (Math
					.random() * notFullBuckets.size())));
		} else {
			bucket = (int) (Math.random() * buckets.size());
		}
		problem = null;
	}

	public void startGame() {
		modelThread = new Thread(new Runnable() {

			public void run() {

				long time = System.currentTimeMillis();
				int sleepTime = 0;

				while (true) {

					long currentTime = System.currentTimeMillis();

					if (changeState.compareAndSet(true, false)) {
						sleepTime = 0;
					}

					if (answerLocked.compareAndSet(true, false)) {
						if (problem.isCorrect(Integer.parseInt(currentAnswer))) {
							informListeners(new GameEvent(
									GameEvent.Type.CORRECT_ANSWER));
							if (correctAnswer()) {
								changeState(State.LEVEL_UP);
							} else {
								changeState(State.MOVING_FUNNEL);
							}
							setNewBucket();
						} else {
							height = -1;
							informListeners(new GameEvent(
									GameEvent.Type.DROP_DOWN));
							sleepTime = 0;
						}
					} else {
						if (currentTime - time >= sleepTime) {
							if (pause.get()) {
								sleepTime = 100000;
							} else {
								switch (currentState) {
								case MENU:
									sleepTime = 100000;
									break;
								case LEVEL_UP:
									informListeners(new GameEvent(
											GameEvent.Type.LEVEL_UP));
									sleepTime = 500;
									currentState = State.MOVING_FUNNEL;
									break;
								case MOVING_FUNNEL:
									if (funnelPosition == bucket * BUCKET_STEPS) {
										setNewProblem();
										changeState(State.DROPPING_PROBLEM);
									} else {
										if (funnelPosition > ((buckets.size() - 1) * BUCKET_STEPS)
												|| funnelPosition < 0) {
											funnelMovingRight = !funnelMovingRight;
										}
										if (funnelMovingRight) {
											int maxBounds = (buckets.size() - 1) * BUCKET_STEPS + 1;
											if (funnelPosition < bucket * BUCKET_STEPS) {
												maxBounds = bucket * BUCKET_STEPS;
											} 
											funnelPosition = Math.min(maxBounds, funnelPosition + speed);
										} else {
											int minBounds = -1;
											if (funnelPosition > bucket * BUCKET_STEPS) {
												minBounds = bucket * BUCKET_STEPS;
											}
											funnelPosition = Math.max(minBounds, funnelPosition - speed);
										}
										informListeners(new GameEvent(
												GameEvent.Type.BUCKET_MOVE));
									}
									sleepTime = 20;
									break;
								case DROPPING_PROBLEM:
									if (height >= 0) {
										height--;
										informListeners(new GameEvent(
												GameEvent.Type.DROP_DOWN));
										sleepTime = 1000 / speed;
									} else {
										if (missProblem()) {
											informListeners(new GameEvent(
													GameEvent.Type.GAME_OVER));
											running = false;
											changeState(State.MENU);
											informListeners(new GameEvent(
													Type.MENU));
										} else {
											setNewBucket();
											changeState(State.MOVING_FUNNEL);
										}
									}
									break;
								}
							}
						} else {
							sleepTime -= (currentTime - time);
						}
						time = currentTime;
					}

					if (!changeState.get() && sleepTime > 0) {
						try {
							Thread.sleep(sleepTime);
						} catch (InterruptedException e) {
						}
					} else {
						Thread.yield();
					}
				}

			}
		});
		modelThread.start();
	}

	private boolean correctAnswer() {
		score += height;
		int oldLevel = level;
		level = score / 100 + 1;
		speed = (level - 1) % 3 + (level - 1) / 3 + 1;
		return oldLevel < level;
	}

	public Problem getProblem() {
		return problem;
	}

	public int getHeight() {
		return height;
	}

	public int getBucket() {
		return bucket;
	}

	public List<Bucket> getBuckets() {
		return buckets;
	}

	private String currentAnswer = "";
	private final Object inputLock = new Object();

	public String getCurrentAnswer() {
		return currentAnswer;
	}

	private void togglePause() {
		if (pause.compareAndSet(true, false)) {
			informListeners(new GameEvent(Type.UNPAUSE));
			changeState.set(true);
		} else if (pause.compareAndSet(false, true)) {
			informListeners(new GameEvent(Type.PAUSE));
			changeState.set(true);
		}
	}

	private void startNewGame(GameMode gameMode) {
		level = 1;
		score = 0;
		speed = 1;

		int i = 0;
		notFullBuckets.clear();
		for (Bucket bucket : buckets) {
			bucket.clear();
			notFullBucketMap.put(bucket, i++);
			notFullBuckets.add(bucket);
		}
		currentGameMode = gameMode;
		running = true;
		setNewBucket();
		changeState(State.MOVING_FUNNEL);
	}

	public void registerKey(char keyChar) {
		synchronized (inputLock) {
			if (currentState == State.MENU) {
				if (Character.isDigit(keyChar)) {
					GameMode gameMode = gameModes
							.get(new Integer("" + keyChar));
					if (gameMode != null) {
						startNewGame(gameMode);
						modelThread.interrupt();
					}
				}
			} else if (keyChar == ' ') {
				togglePause();
				modelThread.interrupt();
			} else if (currentState == State.DROPPING_PROBLEM
					&& !pause.get()
					&& Character.isDigit(keyChar)
					&& (keyChar != '0' || problem.getAnswerLength() == 1 || currentAnswer
							.length() > 0) && !answerLocked.get()) {
				currentAnswer += keyChar;
				if (problem != null
						&& currentAnswer.length() == problem.getAnswerLength()) {
					answerLocked.set(true);
					modelThread.interrupt();
				}
				informListeners(new GameEvent(Type.ANSWERING));
			}
		}
	}

	private void changeState(State state) {
		this.currentState = state;
		changeState.set(true);
	}

	public class GameMode implements Comparable<GameMode> {
		public final int id;
		public final String description;
		public final List<ProblemType> problemTypes;

		public GameMode(int id, String description,
				List<ProblemType> problemTypes) {
			this.id = id;
			this.description = description;
			this.problemTypes = problemTypes;
		}

		public int compareTo(GameMode o) {
			return this.id - o.id;
		}

		@Override
		public boolean equals(Object arg0) {
			if (arg0 instanceof GameMode) {
				return ((GameMode) arg0).id == this.id;
			}
			return false;
		}

		@Override
		public int hashCode() {
			return this.id;
		}

		@Override
		public String toString() {
			return id + ". " + description;
		}

	}

}
